const FAN_RIICHI = 0
const FAN_YAKUHAI = 1
const FAN_TSUMO = 2
const FAN_MENZEN = 3
const FAN_TANYAO = 4
const FAN_PINFU = 5

const FAN_DORA = 6
const FAN_URA_DORA = 7
const FAN_AKA_DORA = 8

const FAN_NAME = ['立直', '役牌', '自摸', '门前', '断幺', '平和', '宝牌', '里宝牌', '赤宝牌']

const BANRANK_TANYAO = (() => {
	let banRank = new Array(111).fill(false)
	for (let suit = 0; suit < 3; ++suit) {
		banRank[1 << 2 | suit] = true
		banRank[9 << 2 | suit] = true
		banRank[40 + (1 << 2 | suit)] = true
		banRank[40 + (9 << 2 | suit)] = true
		banRank[80 + (1 << 2 | suit)] = true
		banRank[80 + (7 << 2 | suit)] = true
	}
	for (let rank = 1; rank <= 7; ++rank) {
		banRank[rank << 2 | 3] = true
		banRank[40 + (rank << 2 | 3)] = true
	}
	return banRank
})()

let pinfuRequiresNoFu = false
let pinfuRequires3Waits = true
let pinfuRequiresNoDora = false

function yakuhaiReqs(tile) {
	return [40 + tile]
}
// This is an estimation, not the accurate answer
function shantenLessThan(hand, s, roundWind, seatWind) {
	let menzen = true
	for (const meld of hand.melds) if (meld[0] === CHOICE_CHOW || meld[0] === CHOICE_PUNG || meld[0] === CHOICE_KONG || meld[0] === CHOICE_ADDED_KONG) {
		menzen = false
		break
	}
	let han2 = 1 + (menzen ? 1 : 0) + ((pinfuRequiresNoFu || pinfuRequires3Waits || pinfuRequiresNoDora) ? 0 : 1) + (_isHu(hand, s, 0, BANRANK_TANYAO) ? 1 : 0)
	if (han2 >= 2) {
		return _isHu(hand, s)
	}
	return han2 && (_isHu(hand, s, 0, 0, [40 + TILE_Z5])
		|| _isHu(hand, s, 0, 0, [40 + TILE_Z6])
		|| _isHu(hand, s, 0, 0, [40 + TILE_Z7])
		|| _isHu(hand, s, 0, 0, [40 + roundWind])
		|| _isHu(hand, s, 0, 0, [40 + seatWind]))
}
function shanten(hand, roundWind, seatWind) {
	for (let joker = 0; ; ++joker) {
		if (joker > 13 || shantenLessThan(hand, joker + 1, roundWind, seatWind)) {
			return joker
		}
	}
}
function hu(hand, tsumo, tenpai, roundWind, seatWind, wanpai, doraCount) {
	if (_isHu(hand)) {
		// Check furiten
		if (!tsumo) {
			let newHand = structuredClone(hand)
			removeElementFromArray(newHand.tiles, tenpai)
			if (isFuriten(newHand)) {
				return undefined
			}
		}

		const tenpaiValue = getTileValue(tenpai)
		let fan = [], counter = newTileCounter()
		countTileArray(counter, hand.tiles)
		countTileMelds(counter, hand.melds)

		// Yakuhai
		let yakuhai = 0
		for (let tile = 23; tile <= 31; tile += 4)if (counter[tile] <= 1) {
			++yakuhai
		}
		if (counter[roundWind] <= 1) {
			++yakuhai
		}
		if (counter[seatWind] <= 1) {
			++yakuhai
		}
		if (yakuhai) {
			fan.push([FAN_YAKUHAI, yakuhai, 0])
		}

		// Menzen
		let menzen = true
		for (const meld of hand.melds) if (meld[0] === CHOICE_CHOW || meld[0] === CHOICE_PUNG || meld[0] === CHOICE_KONG || meld[0] === CHOICE_ADDED_KONG) {
			menzen = false
			break
		}
		if (menzen) {
			fan.push([FAN_MENZEN, 1, 0])
		}

		// Tanyao
		let tanyao = true
		for (const tile of TILE_YAOCHUU) if (counter[tile] < 4) {
			tanyao = false
			break
		}
		if (tanyao) {
			fan.push([FAN_TANYAO, 1, 0])
		}

		// Dora count
		let doraNumber = 0
		// Dora
		for (let i = 0; i < doraCount; ++i) {
			doraNumber += 4 - counter[getTileValue(wanpai[i * 2])]
		}
		// Ura dora
		for (const meld of hand.melds) {
			if (meld[0] === CHOICE_CONCEALED_KONG) {
				doraNumber += 2
			} else if (meld[0] === CHOICE_KONG || meld[0] === CHOICE_ADDED_KONG) {
				doraNumber += 1
			}
		}
		// Aka Dora
		for (const tile of hand.tiles) if (tile & TILE_AKA) {
			++doraNumber
		}
		for (const meld of hand.melds) {
			if (meld[2] & TILE_AKA) {
				++doraNumber
			}
			for (const tile of meld[3]) if (tile & TILE_AKA) {
				++doraNumber
			}
		}

		// Pinfu
		let pinfu = true
		if (pinfuRequiresNoDora && doraNumber) {
			pinfu = false
		}
		if (pinfu && pinfuRequires3Waits && tenpai) {
			let otherTenpais = 0, newHand = structuredClone(hand)
			removeElementFromArray(newHand.tiles, tenpai)
			for (let suit = 0; suit < 4; ++suit) {
				for (let tile = (suit === 3 ? 31 : (36 | suit)); tile > 3; tile -= 4)if (tile !== tenpaiValue && counter[tile]) {
					newHand.tiles.push(tile)
					if (_isHu(newHand, 0)) {
						++otherTenpais
					}
					newHand.tiles.pop()
				}
			}
			if (otherTenpais < 2) {
				pinfu = false
			}
		}
		if (pinfu && pinfuRequiresNoFu) {
			if (menzen && !yakuhai) {
				let hasFu = true
				for (const comb of _combs(hand)) {
					let ok = true, isRyanmenten = false
					for (const set of comb) {
						const type = set[0]
						if (type === CHOICE_CONCEALED_CHOW) {
							const value = set[1] - 80, rank = (value >> 2)
							if ((rank !== 1 && (!tenpai || (tenpai >> 2) === rank + 2)) || (rank !== 7 && (!tenpai || (tenpai >> 2) === rank))) {
								isRyanmenten = true
							}
						} else if (type !== CHOICE_CONCEALED_PAIR) {
							ok = false
							break
						}
					}
					if (ok && isRyanmenten) {
						hasFu = false
						break
					}
				}
				if (hasFu) {
					pinfu = false
				}
			} else {
				pinfu = false
			}
		}
		if (pinfu) {
			fan.push([FAN_PINFU, 1, 0])
		}

		let han2 = 0
		for (const f of fan) {
			han2 += f[1] * 2 + f[2]
		}
		if (han2 < 2 && !tsumo) {
			return undefined
		}
		if (han2 < 4 && !tsumo && !hand.riichi) {
			return undefined
		}

		let fu = 500 * (1 + doraNumber)

		return {
			tsumo: tsumo,
			fan: fan,
			fu: fu,
			han: (han2 >> 1),
			hanHalf: (han2 & 1),
			points: fu << (han2 >> 1),
		}
	} else {
		return undefined
	}
}
function tenpai(hand) {
	if (_isHu(hand, 1)) {
		return {
			points: 1
		}
	} else {
		return undefined
	}
}
function isFuriten(hand) {
	if (tenpai(hand) === undefined) {
		return false
	}
	for (let suit = 0; suit < 4; ++suit) {
		for (let tile = (suit === 3 ? 31 : (36 | suit)); tile > 3; tile -= 4)if (hand.furitenTable[tile]) {
			let newHand = structuredClone(hand)
			newHand.tiles.push(tile)
			if (_isHu(newHand)) {
				return true
			}
		}
	}
	return false
}
function checkAddedMelds(tiles, currentMelds) {
	let melds = []

	// Check added kongs
	for (const meld of currentMelds) if (meld[0] === CHOICE_PUNG) {
		const value = meld[1] - 40
		for (const set of searchSetFromArray(tiles, [
			tile => getTileValue(tile) === value,
		])) {
			melds.push([CHOICE_ADDED_KONG, value + 40, 0, set])
		}
	}
	return melds
}
function checkConcealedMelds(tiles) {
	let melds = []

	// Check kongs
	for (const value of sortAndUnique(tiles.map(getTileValue))) {
		for (const set of searchSetFromArray(tiles, [
			tile => getTileValue(tile) === value,
			tile => getTileValue(tile) === value,
			tile => getTileValue(tile) === value,
			tile => getTileValue(tile) === value,
		])) {
			melds.push([CHOICE_CONCEALED_KONG, value + 40, 0, set])
		}
	}
	return melds
}
function checkMelds(tiles, discarded) {
	const d = getTileValue(discarded)
	let melds = []

	// Check chows
	if ((d & 3) !== 3) {
		// 12[3]
		if (d >= 12) {
			for (const set of searchSetFromArray(tiles, [
				tile => getTileValue(tile) === d - 8,
				tile => getTileValue(tile) === d - 4,
			])) {
				melds.push([CHOICE_CHOW, d + 72, discarded, set])
			}
		}
		// 1[2]3
		if (d >= 8 && d < 36) {
			for (const set of searchSetFromArray(tiles, [
				tile => getTileValue(tile) === d - 4,
				tile => getTileValue(tile) === d + 4,
			])) {
				melds.push([CHOICE_CHOW, d + 76, discarded, set])
			}
		}
		// [1]23
		if (d < 32) {
			for (const set of searchSetFromArray(tiles, [
				tile => getTileValue(tile) === d + 4,
				tile => getTileValue(tile) === d + 8,
			])) {
				melds.push([CHOICE_CHOW, d + 80, discarded, set])
			}
		}
	}

	// Check pungs
	for (const set of searchSetFromArray(tiles, [
		tile => getTileValue(tile) === d,
		tile => getTileValue(tile) === d,
	])) {
		melds.push([CHOICE_PUNG, d + 40, discarded, set])
	}

	// Check kongs
	for (const set of searchSetFromArray(tiles, [
		tile => getTileValue(tile) === d,
		tile => getTileValue(tile) === d,
		tile => getTileValue(tile) === d,
	])) {
		melds.push([CHOICE_KONG, d + 40, discarded, set])
	}
	return melds
}